/*
 * Copyright (c) 2016-2021, Hangzhou Qulian Technology Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "account.h"

// ----------------------------------------------
// account type definition
// ----------------------------------------------
typedef ql_byte_t  account_byte_t;
typedef ql_size_t account_size_t;
typedef ql_err_t   account_err_t;
typedef ql_void_t  account_void_t;
typedef ql_int32_t account_int32_t;
typedef ql_int64_t account_int64_t;

// ----------------------------------------------
// Get Timestamp Function
// ----------------------------------------------
// -- default func --
static account_err_t defaultGettime(account_int64_t* p_dest){
    if(p_dest == NULL)return ACCOUNT_ERR_GETTIME_FUNC;
#if defined(unix) || defined(__linux__) || defined(__unix__) || defined(__unix) || \
    (defined(__APPLE__) && defined(__MACH__)) || defined(uECC_POSIX)
    struct timespec cur_time = {0, 0};
    clock_gettime(CLOCK_REALTIME, &cur_time);
    *p_dest = (ql_int64_t)cur_time.tv_sec * 1000000000 + (ql_int64_t)cur_time.tv_nsec;
#else
    return ACCOUNT_ERR_GETTIME_FUNC;
#endif
    return ACCOUNT_SUCCESS;
}
static Account_GetTime_Function g_time = &defaultGettime;
// -- Set Get Timestamp Function --
account_void_t AccountSetTimeFunc(Account_GetTime_Function p_t){
    g_time = p_t;
}

// ----------------------------------------------
// Account Function
// ----------------------------------------------
// -- init account according private key certificate --
account_err_t AccountInitX509(account_t* acc, account_byte_t* str) {

    account_err_t res;
    account_size_t i;

    // parse key string
    if(strlen((char*)str) == 0) {
        return ACCOUNT_ERR_PARAM_INVAILD;
    }

    x509_key_t key;
    keyInit(&key);
    res = keyParse(&key, str);
    if(res != X509_SUCCESS) {
        keyFree(&key);
        return res;
    }
    memcpy(acc->prikey, key.private_key.data, 32);
    memcpy(acc->pubkey, key.public_key.data, 65);
    keyFree(&key);

    // compute addr
    account_byte_t *pub_hash = calloc(32, sizeof(account_byte_t)); // public key hash, hex format.
    keccak256(acc->pubkey + 1, 64, &pub_hash);
    for(i = 0; i < 20; i ++){   // format address, string format.
        acc->addr[(i+1)*2]   = ((pub_hash[i+12] & 0xf0) >> 4) <=9 ?  ((pub_hash[i+12] & 0xf0) >> 4) + '0' : ((pub_hash[i+12] & 0xf0) >> 4) + 'a' - 10;
        acc->addr[(i+1)*2+1] = ((pub_hash[i+12] & 0x0f)     ) <=9 ?  ((pub_hash[i+12] & 0x0f)     ) + '0' : ((pub_hash[i+12] & 0x0f)     ) + 'a' - 10;
    }
    acc->addr[42] = '\0';
    acc->addr[0] = '0';
    acc->addr[1] = 'x';
    free(pub_hash);

    log_info("addr is : %s",acc->addr);
    return ACCOUNT_SUCCESS;
}

// -- init account according account json --
static account_err_t parseAccountJson(cJSON* items, account_t* acc) {
    account_int32_t i;
    char* data;

    // check version
    cJSON*  version = cJSON_GetObjectItem(items, "version");
    if (version == NULL || strcmp(version -> valuestring , "4.0") != 0) {
        log_debug("Version: incompatible or unreadable");
        return ACCOUNT_ERR_VER_UNMATCH;
    }
    log_debug("account %s: %s", "version", version->valuestring);

    // check algo
    cJSON*  algo = cJSON_GetObjectItem(items, "algo");
    if (algo == NULL || strcmp(algo -> valuestring , "0x03") != 0) {
        log_debug("algo: not supported!");
        return ACCOUNT_ERR_ALGO_UNMATCH;
    }
    log_debug("account %s: %s", "algo", algo->valuestring);

    // check privateKey
    cJSON*  privateKey = cJSON_GetObjectItem(items, "privateKey");
    if (privateKey == NULL) {
        log_debug("privateKey: unreadable!");
        return ACCOUNT_ERR_PRIKEY_UNMATCH;
    }
    if (strlen(privateKey->valuestring) != 64) {
        log_debug("privateKey: Wrong length!");
        return ACCOUNT_ERR_PRIKEY_UNMATCH;
    }
    data = privateKey->valuestring;
    for(i = 0; i < 32; i++){   // format prikey from string to hex.
        account_byte_t a_h = (*(data+i*2  ) <='9' && *(data+i*2  ) >= '0') ? (*(data+i*2  ) - '0') : (*(data+i*2  ) <='f' && *(data+i*2  ) >= 'a' ? *(data+i*2  ) - 'a' + 10 : *(data+i*2  ) - 'A' + 10);
        account_byte_t a_l = (*(data+i*2+1) <='9' && *(data+i*2+1) >= '0') ? (*(data+i*2+1) - '0') : (*(data+i*2+1) <='f' && *(data+i*2+1) >= 'a' ? *(data+i*2+1) - 'a' + 10 : *(data+i*2+1) - 'A' + 10);
        acc->prikey[i] = a_h << 4 | a_l;
    }
    log_debug("account %s: %s", "privateKey", privateKey->valuestring);

    // check publicKey
    cJSON*  publicKey = cJSON_GetObjectItem(items, "publicKey");
    if (publicKey == NULL) {
        log_debug("publicKey: unreadable!");
        return ACCOUNT_ERR_PUBKEY_UNMATCH;
    }
    if (strlen(publicKey->valuestring) != 130) {
        log_debug("publicKey: Wrong length!");
        return ACCOUNT_ERR_PUBKEY_UNMATCH;
    }
    data = publicKey->valuestring;
    for(i = 0; i < 65; i++){   // format prikey from string to hex.
        account_byte_t a_h = (*(data+i*2  ) <='9' && *(data+i*2  ) >= '0') ? (*(data+i*2  ) - '0') : (*(data+i*2  ) <='f' && *(data+i*2  ) >= 'a' ? *(data+i*2  ) - 'a' + 10 : *(data+i*2  ) - 'A' + 10);
        account_byte_t a_l = (*(data+i*2+1) <='9' && *(data+i*2+1) >= '0') ? (*(data+i*2+1) - '0') : (*(data+i*2+1) <='f' && *(data+i*2+1) >= 'a' ? *(data+i*2+1) - 'a' + 10 : *(data+i*2+1) - 'A' + 10);
        acc->pubkey[i] = a_h << 4 | a_l;
    }
    log_debug("account %s: %s", "publicKey", publicKey->valuestring);

    // check address
    cJSON*  address = cJSON_GetObjectItem(items, "address");
    if (address == NULL) {
        log_debug("address: unreadable!");
        return ACCOUNT_ERR_ADDR_UNMATCH;
    }
    if (strlen(address->valuestring) != 40) {
        log_debug("address: Wrong length!");
        return ACCOUNT_ERR_ADDR_UNMATCH;
    }
    memcpy(acc->addr+2, address->valuestring, 40);
    acc->addr[42] = '\0';
    acc->addr[0] = '0';
    acc->addr[1] = 'x';
    log_debug("account %s: %s", "address", acc->addr);

    return ACCOUNT_SUCCESS;
}

account_err_t AccountInitJson(account_t* acc, account_byte_t* str) {

    if(strlen((char*)str) == 0) {
        return ACCOUNT_ERR_PARAM_INVAILD;
    }
    // parse json string
    cJSON* items = cJSON_Parse((char*)str);
    if (items == NULL) {
        log_error("parse json file fail\n");
        return ACCOUNT_ERR_PARAM_INVAILD;
    }
    account_err_t err = parseAccountJson(items, acc);
    // delete buffer
    if (items != NULL) cJSON_Delete(items);

    return err;
}


// ----------------------------------------------
// Transaction Function
// ----------------------------------------------
// -- create transfer transaction --
account_err_t TxSendNew(tx_t* tx, account_t* acc, account_byte_t* to, account_int32_t val, account_byte_t* ex, account_byte_t* exid){

    account_err_t res;
    // Check Param
    if(strlen((char*)to) != 42)  return ACCOUNT_ERR_PARAM_INVAILD;
    // Copy tx element
    memcpy(tx->from, acc->addr, 43);
    memcpy(tx->to, to, 43);
    memcpy(tx->simulate, "false\0", 6);
    memcpy(tx->type, "EVM\0", 4);
    tx->extra   = (ex   == NULL || strlen((char*)ex)   == 0)? (account_byte_t*)"" : ex  ;
    tx->extraid = (exid == NULL || strlen((char*)exid) == 0)? (account_byte_t*)"" : exid;
    tx->value = val;
    // Set time and random
    res = g_time(&tx->timestamp);
    if(res != ACCOUNT_SUCCESS) {
        return res;
    }
    tx->nonce = 1ULL;
    // signature tx
    account_byte_t* hash_prefix = (account_byte_t*)"from=%s&to=%s&value=0x%x&payload=0x0&timestamp=%#llx&nonce=%#llx&opcode=0&extra=%s&vmtype=%s&version=2.5&extraid=%s&cname=";
    account_size_t format_size = strlen((char*)hash_prefix);
    account_size_t tx_size = strlen((char*)tx->from) + strlen((char*)tx->to) + sizeof(tx->value)*8 +
            sizeof(tx->timestamp)*8 + sizeof(tx->nonce)*8 + strlen((char*)tx->extra) + strlen((char*)tx->type) + strlen((char*)tx->extraid);
    if(format_size + tx_size > MAX_BUF_SPACE_SIZE) {
        log_error("The planned space size needs to be greater than %d, but the maximum allowable size is %d", format_size + tx_size, MAX_BUF_SPACE_SIZE);
        return ACCOUNT_ERR_OVER_MAX_SIZE;
    }
    account_byte_t buffer[MAX_BUF_SPACE_SIZE];
    sprintf((char*)buffer, (char*)hash_prefix,
            tx->from, tx->to, tx->value, tx->timestamp, tx->nonce, tx->extra, tx->type, tx->extraid);
    log_debug("need keccak data is : %s",buffer);
    printf("need keccak data is : %s",buffer);

    account_byte_t data_hash[32]; // public key hash, hex format.
    account_byte_t* hash_ptr = data_hash;
    keccak256(buffer, strlen((char*)buffer), &hash_ptr);
    printKeccakData(data_hash);

    account_byte_t sig[65];
    res = uEccSign(acc->prikey, data_hash, sig);
    if (res != ECC_SUCCESS) {
        log_error("tx sign failed: %d", res);
    }
    account_size_t i;
    for(i = 0; i < 65; i ++){
        tx->signature[(i+1)*2]   = ((sig[i] & 0xf0) >> 4) <=9 ?  ((sig[i] & 0xf0) >> 4) + '0' : ((sig[i] & 0xf0) >> 4) + 'a' - 10;
        tx->signature[(i+1)*2+1] = ((sig[i] & 0x0f)     ) <=9 ?  ((sig[i] & 0x0f)     ) + '0' : ((sig[i] & 0x0f)     ) + 'a' - 10;
    }
    tx->signature[0] = '0'; // Identity signature algorithm
    tx->signature[1] = '0';
    tx->signature[132] = '\0';
    log_info("sig: %s",  tx->signature);

    return ACCOUNT_SUCCESS;
}

// -- escape char --
static account_void_t strEscapeStr(account_byte_t* dst, account_byte_t* src){
    account_size_t p=0, q=0;
    account_size_t len = strlen((char*)src);
    for(; p<len; ){
        if(*(src+p) == '"' || *(src+p) == '\\'){
            *(dst+q++) = '\\';
            *(dst+q++) = *(src+p++);
            continue;
        }
        *(dst+q++) = *(src+p++);
    }
}

static account_size_t getIntLength(ql_int64_t val) {
    account_size_t len = 0;
    if (val == 0) return 1;
    if (val < 0) {
        len += 1;
        val = llabs(val);
    }
    while (val > 0) {
        len += 1;
        val /= 10;
    }
    return len;
}

// -- format transaction --
account_err_t TxPrint(tx_t* tx, FORMAT_TYPE fmt_type, account_byte_t* method, account_byte_t* out_buf, account_size_t out_buf_len){

    // len=177 | actual len=153
    account_byte_t tx_fmt_v[] = "{\"jsonrpc\":\"2.0\",\"method\":\"%s\",\"params\":[{\"from\":\"%s\",\"to\":\"%s\",\"value\":%d,\"timestamp\":%llu,\"simulate\":%s,\"nonce\":%llu,\"extra\":\"%s\",\"extraIdString\":%s,\"signature\":\"%s\"}],\"id\":1}";

    // Escape character
    account_size_t len = strlen((char*)tx->extra) * 2 + 1;
    if (len > MAX_BUF_SPACE_SIZE) {
        log_error("The planned usage of escape character space is %d, but the maximum allowable %d", len, MAX_BUF_SPACE_SIZE);
        return ACCOUNT_ERR_OVER_MAX_SIZE;
    }
    account_byte_t* tmp_ext_p = calloc(len, sizeof(account_byte_t));
    if (tmp_ext_p == NULL) {
        return ACCOUNT_ERR_CALLOC_MEMORY;
    }
    strEscapeStr(tmp_ext_p, tx->extra);

    // calculation of space usage
    account_size_t tx_size = strlen((char*)method)         + strlen((char*)tx->from)      + strlen((char*)tx->to)     + getIntLength(tx->value) +
                             getIntLength(tx->timestamp) + strlen((char*)tx->simulate)  + getIntLength(tx->nonce) +  strlen((char*)tmp_ext_p) +
                             strlen((char*)tx->extraid)    + strlen((char*)tx->signature);
    if(153 + tx_size > out_buf_len) {
        log_error("The planned space size needs to be greater than %d, but the actual space size is %d", 153 + tx_size, out_buf_len);
	free(tmp_ext_p);

        return ACCOUNT_ERR_BUF_TOO_SMALL;
    }

    // Format output string
    switch(fmt_type) {
        case ADD_HTTP_BODY: {
            char http_body_fmt[] = "POST / HTTP/1.1\r\nHost: 8.136.20.47:8081\r\nUser-Agent: curl/7.47.0\r\nAccept: */*\r\nContent-Length: %d\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n";
            char fmt[400];
            strcpy(fmt, (char*) http_body_fmt);
            strcpy(fmt+strlen(http_body_fmt),(char*)tx_fmt_v);

            sprintf((char*)out_buf, (char*)fmt, 153 + tx_size, method, tx->from,
                    tx->to, tx->value, tx->timestamp, tx->simulate,
                    tx->nonce, tmp_ext_p, tx->extraid, tx->signature);
            break;
        }
        case ADD_CURL_BODY: {
            char curl_body_fmt_prefix[]  = "curl 8.136.20.47:8081 -X POST -d '";
            char curl_body_fmt_postfix[] = "'";
            char fmt[400];
            strcpy(fmt, (char*) curl_body_fmt_prefix);
            strcpy(fmt+strlen(curl_body_fmt_prefix),(char*)tx_fmt_v);
            strcpy(fmt+strlen(curl_body_fmt_prefix)+strlen((char*)tx_fmt_v) , curl_body_fmt_postfix);

            sprintf((char*)out_buf, (char*)fmt, method, tx->from,
                    tx->to, tx->value, tx->timestamp, tx->simulate,
                    tx->nonce, tmp_ext_p, tx->extraid, tx->signature);
            break;
        }
        case ORIGIN_MSG:{
            sprintf((char*)out_buf, (char*)tx_fmt_v, method, tx->from,
                    tx->to, tx->value, tx->timestamp, tx->simulate,
                    tx->nonce, tmp_ext_p, tx->extraid, tx->signature);
            break;
        }
    }

    free(tmp_ext_p);

    return ACCOUNT_SUCCESS;
}
